iT邦幫忙

2024 iThome 鐵人賽

DAY 6
0

pdf檔也是常見的檔案格式,這次的範例是想嘗試在Spring boot的環境中,查詢實際的DB,將資料整理後匯出pdf。
想模擬的情境是學校學生成績的相關報表。

情境:學生基本資料PDF

DB與Mock Data

  • Mock Data我是用Mockaroo生出來的,一用發現很好用,可以設定關聯,還可以直接匯出Create Table和Insert資料的SQL,非常方便
  • DB我使用MySQL,只是建立簡單的四張Table(學生、科系、課程、考試分數)用來模擬情境

設計報表

查詢資料

以SQL查詢Student Table,確認是我們要的資料

SELECT * FROM ithome2024.student;

在MySQl中看到原始資料大概有這些

建立Entity

我常使用JPA搭配Querydsl,先建立映射對象Entity

@Entity
@Table(name = "student", schema = "ithome2024")
public class StudentEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "Student_Id")
    private Integer studentId;

    @Column(name = "First_Name")
    private String firstName;

    @Column(name = "Last_Name")
    private String lastName;

    @Column(name = "Gender")
    private String gender;

    @Column(name = "Grade")
    private String grade;

    @Column(name = "Department_Id")
    private Integer departmentId;
}

DAO

Querydsl很像SQL語法,還可以透過Projections.bean自動將查詢結果的列對應到DTO的屬性上。

@Repository
public class JasperReportDemoDaoImpl implements IJasperReportDemoDao {

    @Autowired
    private JPQLQueryFactory queryFactory;

    @Override
    public List<StudentAndDepartmentDto> getStudentAndDepartmentData() {
        QStudentEntity qStudent = QStudentEntity.studentEntity;
        QDepartmentEntity qDepartment = QDepartmentEntity.departmentEntity;

        return queryFactory.select(Projections
                    .bean(StudentAndDepartmentDto.class,
                    qStudent.studentId, qStudent.firstName, qStudent.lastName,
                    qStudent.gender, qStudent.grade,
                    qDepartment.departmentId, qDepartment.departmentName,
                    qDepartment.departmentDesc))
                .from(qStudent)
                .innerJoin(qDepartment)
                .on(qStudent.departmentId.eq(qDepartment.departmentId))
                .fetch();
    }
}

Service

由於這次情境很單純,Service沒有什麼邏輯要處理,僅是做一些資料轉換

@Service
public class ReportDemoServiceImpl implements IReportDemoService {
    @Autowired
    private IJasperReportDemoDao jasperReportDemoDao;

    @Override
    public List<StudentDataReportModel> getStudentAndDepartmentData() {
        List<StudentDataReportModel> studentDataReportModelList = null;
        try {
            studentDataReportModelList = Optional
                    .of(jasperReportDemoDao.getStudentAndDepartmentData())
                    .orElse(new ArrayList<>())
                    .stream().map(StudentDataReportModel::new)
                    .collect(Collectors.toList());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        return studentDataReportModelList;
    }
}

資料轉換的部分放在Model的建構子中

@Data
@AllArgsConstructor
@NoArgsConstructor
public class StudentDataReportModel {
    private Integer studentId;

    private String fullName;

    private String gender;

    private String grade;

    private String departmentDesc;

    public StudentDataReportModel(StudentAndDepartmentDto dto) {
        this.studentId = dto.getStudentId();
        this.fullName = dto.getFirstName() + " " + dto.getLastName();
        this.gender = "Male".equals(dto.getGender()) ? "男" : "女";
        this.grade = dto.getGrade();
        this.departmentDesc = dto.getDepartmentDesc();
    }
}

Facade

  1. 從Service取得的List<StudentDataReportModel>就是這次要放進報表的DataSource
// 1. 查詢學生基本資料
List<StudentDataReportModel> studentDataReportModelList = 
                      reportDemoService.getStudentAndDepartmentData();
  1. 我們模板的Parameters有date(資料時間)與studentNum(學生人數),將設定好的值放入REPORT_PARAMETERS_MAP
// 2. 設定報表參數
Map<String, Object> parametersMap = 
                this.getStudentDataParameters(studentDataReportModelList);
                    
private Map<String, Object> getStudentDataParameters(
            List<StudentDataReportModel> studentDataReportModelList) {
    Map<String, Object> parameters = new HashMap<>();
    
    LocalDate localDate = new Date().toInstant()
                        .atZone(ZoneId.systemDefault()).toLocalDate();
    parameters.put("date", localDate
                   .format(DateTimeFormatter.ofPattern("yyyy年MM月dd日")));
                   
    parameters.put("studentNum", studentDataReportModelList.size());
    return parameters;
}
  1. 接著將資料放入模板並編譯,最後轉換為byte [],我們可以將這個邏輯封裝進Util中,寫成templateToPdfByteSimple方法
  • 上一篇匯出Excel有JRXlsxExporter,匯出pdf當然也有JRPdfExporter,用法都一樣,只要根據檔案類型使用不同類別就好了
  • 如果只是單純匯出,沒有進行其他設定,也可以使用JasperExportManager,這個工具有匯出pdf、xml、html的方法,不過沒有辦法匯出excel檔
public class ExportReportUtil {
    // 以DataSource、報表路徑、parametersMap作為參數,在Util中處理報表生命週期
    public static byte[] templateToPdfByteSimple(List dataSourceList, 
                String reportPath, Map<String, Object> parametersMap
                                                    ) throws Exception {

        try {
            // 以JasperCompileManager將jrxml模板編譯成jasper文件
            JasperReport jasperReport = JasperCompileManager
                                .compileReport(ExportReportUtil
                                .class.getResourceAsStream(reportPath));

            // 將Java集合資料來源與Jasper報表進行綁定
            JRDataSource dataSource = 
                        new JRBeanCollectionDataSource(dataSourceList, true);

            // 將資料填入報表
            JasperPrint print = JasperFillManager
                       .fillReport(jasperReport, parametersMap, dataSource);

            // 匯出為pdf
            return JasperExportManager.exportReportToPdf(print);
        } catch (Exception e) {
            throw new Exception();
        }
    }
}

方法中宣告模板路徑,並將第一步的DataSource、第二步的parametersMap都作為參數傳入templateToPdfByteSimple方法

// 3.匯出excel byte[]
byte[] bytes = null;
try {
    String reportPath = "/Report/Jasper/StudentDataReport.jrxml";
    bytes = ExportReportUtil
                .templateToPdfByteSimple(studentDataReportModelList, 
                                          reportPath, parametersMap);
} catch (Exception e) {
    throw new RuntimeException(e);
}
  1. 檔案名稱前後端都可以處理,放在後端處理的話,有中文必須先encode再傳到前端,否則會有亂碼
// 4.設定檔案名稱
String encodedFilename = null;
try {
    encodedFilename = URLEncoder
        .encode("學生與科系資料." + fileType, StandardCharsets.UTF_8.name());
} catch (Exception e) {
    throw new RuntimeException(e);
}

匯出pdf

最後打開匯出的pdf,但越看越不對勁...
中文字都不見了!

google之後發現這個問題存在已久,也有不少解決方案,就在下一篇說明吧


Reference


上一篇
JasperReports-用Java匯出Excel範例報表
下一篇
JasperReports-PDF中文無法顯示的問題
系列文
Java工程師的報表入門與實作30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言